iT邦幫忙

2024 iThome 鐵人賽

DAY 22
0
Software Development

一起看無間道學EdgeDB系列 第 22

[Day22] - 九幕:真相大白

  • 分享至 

  • xImage
  •  

Full schema preview

本日所有schema搶先看

劇情提要

建明得知黃sir將與警方臥底於大廈見面,通知韓琛。韓琛一面派手下到大廈,一面進行毒品交易。黃sir為掩護永仁離開,被韓琛手下丟下大廈,寧願殉職而不發一言。黃sir死後,建明聯手永仁於停車場擊斃韓琛,最終兩人於警察局見面。當建明正幫永仁處理臥底檔案時,永仁發現其親手所寫帶有「標」字的信封竟然在建明桌上,頓時醒悟原來建明就是韓琛派至警隊的臥底,立即悄然離開。

scene09

此劇照引用自IMDb-無間道

警隊資安升級計畫

自從黃sir殉職之後,警隊高層了解在趕快找出韓琛臥底的同時,也需要保護好自己派出的臥底,於是決定全面重新檢查一遍資料庫的存取權限。

經過一番資安演練,IT部門也發現平行時空的建明所發現的漏洞,於是高層決定做出以下變更:

  • PoliceSpy新增兩個access policy
    • 只有副處長級別以上(DCP)可以insertupdatedelete
    • 只有警司級別以上(SP)可以select
  • 新增一個PoliceSpyFile方便各部門協同操作,只有警司級別以上(SP)可以執行全部操作。
  • 於非內網登入系統時,不提供REPL操作。關於臥底資料僅提供一個呼叫list_police_spy_names()的endpoint,且只有當操作者驗證為警司級別以上(SP)且密碼正確的情況下,才能取得警隊全部臥底的名字。

EdgeQL query

建立 global current_user_id

current_user_id是一個global scalar,讓我們在全域中都可以存取這個值。

global current_user_id: uuid;

可以透過set這個指令來給定其值,如:

set global current_user_id:=<uuid>"ccc7a858-bd17-11ee-b4be-9f69662124af";

或透過reset將其回復為預設值,如:

reset global current_user_id;

由於我們沒有給定預設值,所以如果執行上述query時,會將global current_user_id變為空的<uuid>{}

需要留意global為關鍵字,所以存取global scalar時,global關鍵字不可省略。

更新PoliceSpy

PoliceSpy新增兩個access policy

type PoliceSpy extending Character, IsSpy {
    access policy authorized_allow_insert_update_delete
      allow insert, update, delete
      using (
      with police_officer:= (select IsPolice 
                             filter .id = global current_user_id),
      select if exists police_officer then (
              police_officer.police_rank ?? PoliceRank.PC >= PoliceRank.DCP
          ) else (
              false
          )
      ){errmessage := "PoliceRank required: PoliceRank.DCP"};

    access policy authorized_allow_select
      allow select
      using (
      with police_officer:= (select IsPolice 
                             filter .id = global current_user_id),
      select if exists police_officer then (
              police_officer.police_rank ?? PoliceRank.PC >= PoliceRank.SP
          ) else (
              false
          )
      ){errmessage := "PoliceRank required: PoliceRank.SP"};
}

我們在Envelope已經學習過access policy,這裡比較不一樣的是using內比較複雜,我們舉第一個access policy為例來看:

  • with區塊內,確認global current_user_id現在所指定的id的確在IsPolice中。

  • 接著在select中使用if cond then {} else {}的語法來判斷需要執行的query。我們用的判斷式是exists police_officer

    • 如果police_officer存在的話,我們執行police_officer.police_rank ?? PoliceRank.PC >= PoliceRank.DCP。這段query的意思是判斷所找到police_officerpolice_rank是否高於PoliceRank.DCP,如果是的話,回傳true,否則回傳false。其中??是當police_officer.police_rank為空set時的預設值,我們預設其為官階最小的PoliceRank.PC
    • 如果police_officer不存在的話,回傳false

這麼一來,我們滿足了第一個需求。

建立PoliceSpyFile

PoliceSpyFile extending Archive而來,有一個link、一個property及一個access policy

  • colleaguesmulti link指向PoliceSpy
  • classified_info property為一str,代表所儲存的機密資訊。
  • access policy的寫法與PoliceSpy類似,但這邊是警司級別以上(SP)可以執行全部操作。
type PoliceSpyFile extending Archive {
    multi colleagues: PoliceSpy;
    classified_info: str; 

    access policy authorized_allow_all
      allow all
      using (
      with police_officer:= (select IsPolice 
                             filter .id = global current_user_id),
      select if exists police_officer then (
              police_officer.police_rank ?? PoliceRank.PC >= PoliceRank.SP
          ) else (
              false
          )
      ){ errmessage := "PoliceRank required: PoliceRank.SP"};
}

新增PoliceSpyFile的妙用

警司級別以上(SP)可以對PoliceSpyFile進行全部操作,包括select colleagues,這有可能會選取到多個PoliceSpy。但要對這些PoliceSpy進行updatedelete依然需要為副處長級別以上(DCP)。

PoliceSpyFile可以想成一個專案簡報,當您具備足夠權限的時候,可以對這個簡報做任何操作,包括引用專案檔案(但如果權限不足的話,將無法更新或刪除專案檔案)。

這麼一來,我們滿足了第二個需求。

編寫list_police_spy_names()

list_police_spy_names()大多數情況應該會被某種web framework寫出來的程式所呼叫(例如PythonFastAPIRustAxum)。

假如您有一個/policespy-names的endpoint,可以用GET來取得所有PolicySpy nameJSON格式,那麼處理這個endpoint的view function很有可能可以借助我們所寫的list_police_spy_names()

list_police_spy_names()接收一個為strcode參數,並返回JSON格式:

  • with區塊,透過validate_password()做驗證。如果通過的話,則返回所有PoliceSpyFile,否則返回空set(即<PoliceSpyFile>{})。
  • with區塊,透過array_agg()police_spy_file.colleagues.name轉為array,並存為names
  • 最後利用json_object_pack()<json>(names)casting功能返回JSON格式。
function list_police_spy_names(code: str) -> json
using (
    with police_spy_file:= PoliceSpyFile if validate_password(code)
                           else <PoliceSpyFile>{},
         names:= array_agg(police_spy_file.colleagues.name),
    select json_object_pack({("names", <json>(names))})
);

要完成validate_password()還需要搭配使用ext::pgcryptomorse_code_of_undercoverget_stored_encrypted_password(),我們繼續看下去。

使用ext::pgcrypto

...
using extension pg_trgm;

module default {
    ...
}

另外,如果您的app有驗證需求的話,可以試試EdgeDB4.0推出的Auth extension

建立alias morse_code_of_undercover

劇中永仁臥底檔案的密碼就是臥底的摩斯密碼

根據網路上的搜尋結果,摩斯密碼大多是使用-,但劇中卻是使用_。讓我們尊重原著,使用內建的str_replace()將臥底的摩斯密碼中的-換成_,並存成alias方便使用。

alias morse_code_of_undercover:= str_replace("..- -. -.. . .-. -.-. --- ...- . .-.", "-", "_");

實務上,不應該將機密資訊存為alias。我們這麼做只是方便稍後展示validate_password()list_police_spy_names()的效果。

編寫測試aliasfunction

新增test_scene09_alias()並更新test_alias()

function test_alias() -> bool
using (all({
        test_scene01_alias(),
        test_scene02_alias(),
        test_scene03_alias(),
        test_scene05_alias(),
        test_scene09_alias(),
    })
);

function test_scene09_alias() -> bool
using (all({
        (exists morse_code_of_undercover),
    })
);

編寫get_stored_encrypted_password()

get_stored_encrypted_password()模擬自資料庫中取出hash過的加密密碼(雖然在這邊它看起來只是每次被呼叫時,計算morse_code_of_undercover的hash值)。

function get_stored_encrypted_password() -> str
using (
    with code:= morse_code_of_undercover,
            module ext::pgcrypto, 
    select crypt(code, gen_salt())
);

編寫validate_password()

最後我們參考官方文件中的範例,使用ext::pgcrypto::crypt()來計算所輸入的密碼(salt為hash過的加密密碼),是否會等於hash過的加密密碼本身。如果是的話,代表我們輸入的是正確密碼,返回true,否則則返回false

function validate_password(code: str) -> bool
using (
    with hash:= get_stored_encrypted_password(),
            module ext::pgcrypto,
    select crypt(code, hash) = hash
);

至此,我們滿足了第三個需求。

執行end migration

did you create extension 'pgcrypto'? [y,n,l,c,b,s,q,?]
> y
did you create alias 'default::morse_code_of_undercover'? [y,n,l,c,b,s,q,?]
> y
did you create function 'default::get_stored_encrypted_password'? [y,n,l,c,b,s,q,?]
> y
did you create function 'default::test_scene09_alias'? [y,n,l,c,b,s,q,?]
> y
did you create function 'default::validate_password'? [y,n,l,c,b,s,q,?]
> y
did you create global 'default::current_user_id'? [y,n,l,c,b,s,q,?]
> y
did you create object type 'default::PoliceSpyFile'? [y,n,l,c,b,s,q,?]
> y
did you create function 'default::list_police_spy_names'? [y,n,l,c,b,s,q,?]
> y
did you alter function 'default::test_alias'? [y,n,l,c,b,s,q,?]
> y
did you alter object type 'default::PoliceSpy'? [y,n,l,c,b,s,q,?]
> y

由於我們添加了兩個access policyPoliceSpy,從現在開始每次select PoliceSpy時,都要時刻注意global current_user_id所屬的object是否有足夠權限。

測試

測試test_alias()

由於test_alias()中的test_scene09_alias()含有chenPoliceSpy)的測試,為了能夠selectchen來進行測試,我們從Police中隨意挑選一個PoliceRankSPobject,將此objectid指定給global current_user_id(由於目前SP等級的警察只有黃sir一個,所以這個query就是將global current_user_id設為黃sir的id)。測試完成後,再執行reset global current_user_id回復為預設值。

# end migration needs to be applied before running this query
set global current_user_id:= (select Police filter .police_rank=PoliceRank.SP limit 1).id;

select test_alias();

reset global current_user_id;

測試validate_password()

如果輸入正確的密碼,validate_password()會回傳true,否則回傳false

select validate_password(morse_code_of_undercover); # {true}
select validate_password("27149"); # {false}

測試PoliceSpyPoliceSpyFilelist_police_spy_names()

如果PoliceRankPoliceRank.SP

test_alias一樣,我們將global current_user_id設為黃sir的id

set global current_user_id:= (select Police filter .police_rank=PoliceRank.SP limit 1).id;

接著進行PoliceSpy各項操作測試:

  • insert會得到AccessPolicyError
edgedb error: AccessPolicyError: access policy violation on insert of default::PoliceSpy (PoliceRank required: PoliceRank.DCP)
  • select可以正常執行。
  • updatedelete會得到空set
select PoliceSpy;

update PoliceSpy
filter .name="陳永仁"
set {
    nickname:= .nickname ++ "!",
}; # {}

delete PoliceSpy filter .name="陳永仁"; # {}

這樣的結果符合需求一的部份要求。

再來進行PoliceSpyFile各項操作測試及list_police_spy_names()功能測試:

  • PoliceSpyFile的各項功能皆能成功操作。
  • list_police_spy_names()在密碼正確的情況下,回傳含有資訊的JSON資料,否則回傳空的JSON資料。

這樣的結果符合需求二及需求三。

insert PoliceSpyFile {
    colleagues:= chen,
    classified_info:= "Handler: test_SP...",
};

select PoliceSpyFile; 

# {Json("{\"names\": [\"陳永仁\"]}")}
select list_police_spy_names(morse_code_of_undercover);

# {Json("{\"names\": []}")}
select list_police_spy_names("abc");

update PoliceSpyFile filter .classified_info="Handler: test_SP..."
set {
    classified_info:= .classified_info ++ "..."
};

delete PoliceSpyFile;

回復global current_user_id為預設值。

reset global current_user_id;
如果PoliceRankPoliceRank.DCP

由於資料庫中還沒有PoliceRankDCPPolice object,所以我們先insert一個,再將其id指定給global current_user_id

insert Police {name:= "test_DCP", police_rank:=PoliceRank.DCP};
set global current_user_id:= (select Police filter .police_rank=PoliceRank.DCP limit 1).id;

接著進行PoliceSpy各項操作測試,皆能成功操作。

這樣的結果加上PoliceRankPoliceRank.SP的測試,符合需求一的全部要求。

Insert PoliceSpy {name:= "test_police_spy_by_DPC"};

select PoliceSpy;

update PoliceSpy filter .name="test_police_spy_by_DPC"
set {
    nickname:= "test_police_spy_by_DPC",
};

delete PoliceSpy filter .nickname="test_police_spy_by_DPC";

再來進行PoliceSpyFile各項操作測試及list_police_spy_names()功能測試:

  • PoliceSpyFile的各項功能皆能成功操作。
  • list_police_spy_names()在密碼正確的情況下,回傳含有資訊的JSON資料,否則回傳空的JSON資料。

這樣的結果符合需求二及需求三。

insert PoliceSpyFile {
    colleagues:= chen,
    classified_info:= "Handler: test_DCP...",
};

select PoliceSpyFile; 

# {Json("{\"names\": [\"陳永仁\"]}")}
select list_police_spy_names(morse_code_of_undercover);

# {Json("{\"names\": []}")}
select list_police_spy_names("abc");

update PoliceSpyFile filter .classified_info="Handler: test_DCP..."
set {
    classified_info:= .classified_info ++ "..."
};

delete PoliceSpyFile;

insert ChenLauContact

insert ChenLauContact {
    how:= "電話",
    detail:= "黃sir殉職後,建明以黃sir手機聯絡永仁",
    `when`:= assert_single((select FuzzyTime filter .fuzzy_fmt="2002/11/23_HH24:MI:SS_ID")),
    where:= police_station union (insert Location {name:= "電車站"}),
};

insert ChenLauContact {
    how:= "面對面",
    detail:= "建明擊斃韓琛後,終於在警局與永仁見面,並確認其臥底身份。",
    `when`:= assert_single((select FuzzyTime filter .fuzzy_fmt="2002/11/23_HH24:MI:SS_ID")),
    where:= police_station,
};

insert此場景的Scene

insert Scene {
      title:= "真相大白", 
      detail:= "建明得知黃sir將與警方臥底於大廈見面,通知韓琛。韓琛一面派" ++
               "手下到大廈,一面進行毒品交易。黃sir為掩護永仁離開,被韓琛" ++
               "手下丟下樓,寧願殉職而不發一言。黃sir死後,建明聯手永仁於" ++
               "停車場擊斃韓琛,最終兩人於警察局見面。當建明正幫永仁處理臥" ++
               "底檔案時,永仁發現其親手所寫帶有「標」字的信封竟然在建明桌上," ++
               "醒悟原來建明就是韓琛派至警隊的臥底,立即悄然離開。",
      who:= (select Gangster filter .nickname in {"迪路", "傻強"}) 
             union {wong, chen, hon, lau},
      `when`:= assert_single(
          (select FuzzyTime filter .fuzzy_fmt="2002/11/23_HH24:MI:SS_ID")
      ),
      where:=  (select Location filter .name in {"天台", "電車站"}) union 
               police_station union
               (select(insert Location {name:="停車場"})),         
};
# --8

最後清理

刪除PoliceRankDCP的測試用Police object,並回復global current_user_id為預設值。

delete Police filter .name="test_DCP";

reset global current_user_id;

無間假設

我們假設劇中只有一個天台Location object

無間吹水

建明要刪掉永仁臥底檔案時,鏡頭內所帶到的資訊及其臥底時間,多處都與三部曲劇情不相吻合。

參考資料

無間EdgeDB九幕:真相大白


上一篇
[Day21] - 八幕:誰是內鬼
下一篇
[Day23] - 十幕:我想做個好人
系列文
一起看無間道學EdgeDB30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言